<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <h1 data-lake-id="JzP2s" id="JzP2s"><span data-lake-id="u05fb9a3f" id="u05fb9a3f">典型回答</span></h1>
  <p data-lake-id="u74d29785" id="u74d29785"><br></p>
  <p data-lake-id="uaadad8f8" id="uaadad8f8"><span data-lake-id="u77289d59" id="u77289d59">不一定，在HotSpot虚拟机中，存在JIT优化的机制，JIT优化中可能会进行逃逸分析，当经过逃逸分析发现某一个局部对象没有逃逸到线程和方法外的话，那么这个对象就可能不会在堆上分配内存，而是进行栈上分配。</span></p>
  <p data-lake-id="u737224d4" id="u737224d4"><br></p>
  <h1 data-lake-id="tTXow" id="tTXow"><span data-lake-id="u72528ad7" id="u72528ad7">扩展知识</span></h1>
  <p data-lake-id="u46060db5" id="u46060db5"><br></p>
  <h3 data-lake-id="S0gWS" id="S0gWS"><span data-lake-id="u00f74580" id="u00f74580">逃逸分析</span></h3>
  <p data-lake-id="u9a311b6b" id="u9a311b6b"><br></p>
  <p data-lake-id="u9122214c" id="u9122214c"><br></p>
  <h3 data-lake-id="MsOh2" id="MsOh2"><span data-lake-id="u05b4ea51" id="u05b4ea51">标量替换</span></h3>
  <p data-lake-id="ubc05b450" id="ubc05b450"><br></p>
  <p data-lake-id="ud82e34e1" id="ud82e34e1"><span data-lake-id="udd16f127" id="udd16f127">标量（Scalar）是指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量。相对的，那些还可以分解的数据叫做聚合量（Aggregate），Java中的对象就是聚合量，因为他可以分解成其他聚合量和标量。</span></p>
  <p data-lake-id="u15a95fb3" id="u15a95fb3"><span data-lake-id="u9c10ad41" id="u9c10ad41">​</span><br></p>
  <p data-lake-id="u18987158" id="u18987158"><span data-lake-id="ufb51ad28" id="ufb51ad28">在JIT阶段，如果经过逃逸分析，发现一个对象不会被外界访问的话，那么经过JIT优化，就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。</span></p>
  <p data-lake-id="u15a37a3e" id="u15a37a3e"><span data-lake-id="u404d730a" id="u404d730a">​</span><br></p>
  <pre lang="java"><code>
public static void main(String[] args) {
   alloc();
}

private static void alloc() {
   Point point = new Point（1,2）;
   System.out.println("point.x="+point.x+"; point.y="+point.y);
}
class Point{
    private int x;
    private int y;
}
</code></pre>
  <p data-lake-id="uf6c8e21a" id="uf6c8e21a"><span data-lake-id="u33d84d8b" id="u33d84d8b">​</span><br></p>
  <p data-lake-id="uc2a124f5" id="uc2a124f5"><span data-lake-id="u2c6489df" id="u2c6489df">以上代码中，point对象并没有逃逸出alloc方法，并且point对象是可以拆解成标量的。那么，JIT就不会直接创建Point对象，而是直接使用两个标量int x ，int y来替代Point对象。</span></p>
  <p data-lake-id="u15643fb0" id="u15643fb0"><span data-lake-id="u807e4b0e" id="u807e4b0e">​</span><br></p>
  <p data-lake-id="ub69c6c26" id="ub69c6c26"><span data-lake-id="u6058e21a" id="u6058e21a">以上代码，经过标量替换后，就会变成：</span></p>
  <p data-lake-id="u3945ee81" id="u3945ee81"><span data-lake-id="uffb06501" id="uffb06501">​</span><br></p>
  <pre lang="java"><code>
private static void alloc() {
   int x = 1;
   int y = 2;
   System.out.println("point.x="+x+"; point.y="+y);
}
</code></pre>
  <p data-lake-id="ub898ae8d" id="ub898ae8d"><span data-lake-id="u0a266532" id="u0a266532">​</span><br></p>
  <p data-lake-id="u5bb7d80c" id="u5bb7d80c"><span data-lake-id="u5d0a5915" id="u5d0a5915">可以看到，Point这个聚合量经过逃逸分析后，发现他并没有逃逸，就被替换成两个标量了。那么标量替换有什么好处呢？就是可以大大减少堆内存的占用。因为一旦不需要创建对象了，那么就不再需要分配堆内存了。</span></p>
  <p data-lake-id="uc34e2d30" id="uc34e2d30"><span data-lake-id="uadd81849" id="uadd81849">​</span><br></p>
  <p data-lake-id="u3542d9b2" id="u3542d9b2"><span data-lake-id="ubb19579c" id="ubb19579c">标量替换为栈上分配提供了很好的基础。</span></p>
  <h3 data-lake-id="ERYAM" id="ERYAM"><span data-lake-id="uf5f73576" id="uf5f73576">栈上分配</span></h3>
  <p data-lake-id="u5e73a6ad" id="u5e73a6ad"><br></p>
  <p data-lake-id="ue368baa1" id="ue368baa1"><span data-lake-id="u57a8e324" id="u57a8e324">我们知道，在一般情况下，对象和数组元素的内存分配是在堆内存上进行的。但是随着JIT编译器的日渐成熟，很多优化使这种分配策略并不绝对。JIT编译器就可以在编译期间根据逃逸分析的结果，来决定是否可以将对象的内存分配从堆转化为栈。</span></p>
  <p data-lake-id="u6060314b" id="u6060314b"><br></p>
  <p data-lake-id="ufaa73924" id="ufaa73924"><span data-lake-id="u8c6d7851" id="u8c6d7851">我们来看以下代码：</span></p>
  <p data-lake-id="u267cb01f" id="u267cb01f"><br></p>
  <pre lang="java"><code>
public static void main(String[] args) {
    long a1 = System.currentTimeMillis();
    for (int i = 0; i &lt; 1000000; i++) {
        alloc();
    }
    // 查看执行时间
    long a2 = System.currentTimeMillis();
    System.out.println("cost " + (a2 - a1) + " ms");
    // 为了方便查看堆内存中对象个数，线程sleep
    try {
        Thread.sleep(100000);
    } catch (InterruptedException e1) {
        e1.printStackTrace();
    }
}

private static void alloc() {
    User user = new User();
}

static class User {

}
</code></pre>
  <p data-lake-id="u6e1725d5" id="u6e1725d5"><br></p>
  <p data-lake-id="u6b80983e" id="u6b80983e"><span data-lake-id="u3308a033" id="u3308a033">其实代码内容很简单，就是使用for循环，在代码中创建100万个User对象。</span></p>
  <p data-lake-id="udc6482c6" id="udc6482c6"><br></p>
  <p data-lake-id="u09fe6dc7" id="u09fe6dc7"><span data-lake-id="u5fb236ca" id="u5fb236ca">我们在alloc方法中定义了User对象，但是并没有在方法外部引用他。也就是说，这个对象并不会逃逸到alloc外部。经过JIT的逃逸分析之后，就可以对其内存分配进行优化。</span></p>
  <p data-lake-id="u62ef37c5" id="u62ef37c5"><br></p>
  <p data-lake-id="u58bb1507" id="u58bb1507"><span data-lake-id="ub9e58fe7" id="ub9e58fe7">我们指定以下JVM参数并运行：</span></p>
  <p data-lake-id="uae1c095b" id="uae1c095b"><br></p>
  <pre lang="java"><code>
-Xmx4G -Xms4G -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
</code></pre>
  <p data-lake-id="u765dd15c" id="u765dd15c"><br></p>
  <p data-lake-id="u9cb35f79" id="u9cb35f79"><span data-lake-id="u91d88d8e" id="u91d88d8e">在程序打印出 </span><code data-lake-id="u4ba1496f" id="u4ba1496f"><span data-lake-id="u24879f14" id="u24879f14">cost XX ms</span></code><span data-lake-id="ua57c4098" id="ua57c4098"> 后，代码运行结束之前，我们使用</span><code data-lake-id="u0ecf88c5" id="u0ecf88c5"><span data-lake-id="ud88004b9" id="ud88004b9">[jmap][1]</span></code><span data-lake-id="ub5b13d23" id="ub5b13d23">命令，来查看下当前堆内存中有多少个User对象：</span></p>
  <p data-lake-id="ua990471e" id="ua990471e"><br></p>
  <pre lang="java"><code>
➜  ~ jps
2809 StackAllocTest
2810 Jps
➜  ~ jmap -histo 2809

 num     #instances         #bytes  class name
----------------------------------------------
   1:           524       87282184  [I
   2:       1000000       16000000  StackAllocTest$User
   3:          6806        2093136  [B
   4:          8006        1320872  [C
   5:          4188         100512  java.lang.String
   6:           581          66304  java.lang.Class
</code></pre>
  <p data-lake-id="uae678063" id="uae678063"><br></p>
  <p data-lake-id="u0c599ddd" id="u0c599ddd"><span data-lake-id="u5a9c32fc" id="u5a9c32fc">从上面的jmap执行结果中我们可以看到，堆中共创建了100万个</span><code data-lake-id="ubabe4248" id="ubabe4248"><span data-lake-id="u0070535f" id="u0070535f">StackAllocTest$User</span></code><span data-lake-id="ud82b12fd" id="ud82b12fd">实例。</span></p>
  <p data-lake-id="ufb87224c" id="ufb87224c"><br></p>
  <p data-lake-id="ue7755e3b" id="ue7755e3b"><span data-lake-id="u2e532fc0" id="u2e532fc0">在关闭逃避分析的情况下（-XX:-DoEscapeAnalysis），虽然在alloc方法中创建的User对象并没有逃逸到方法外部，但是还是被分配在堆内存中。也就说，如果没有JIT编译器优化，没有逃逸分析技术，正常情况下就应该是这样的。即所有对象都分配到堆内存中。</span></p>
  <p data-lake-id="u122400dc" id="u122400dc"><br></p>
  <p data-lake-id="u236abc22" id="u236abc22"><span data-lake-id="u551c782c" id="u551c782c">接下来，我们开启逃逸分析，再来执行下以上代码。</span></p>
  <p data-lake-id="ufbde2b94" id="ufbde2b94"><br></p>
  <pre lang="java"><code>
-Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
</code></pre>
  <p data-lake-id="uc59c4494" id="uc59c4494"><br></p>
  <p data-lake-id="uf6fed43c" id="uf6fed43c"><span data-lake-id="u1a54d186" id="u1a54d186">在程序打印出 </span><code data-lake-id="uf1efd645" id="uf1efd645"><span data-lake-id="u8edd2c3a" id="u8edd2c3a">cost XX ms</span></code><span data-lake-id="ub78e8532" id="ub78e8532"> 后，代码运行结束之前，我们使用</span><code data-lake-id="u1db741af" id="u1db741af"><span data-lake-id="u984d9247" id="u984d9247">jmap</span></code><span data-lake-id="u3e3d4d4a" id="u3e3d4d4a">命令，来查看下当前堆内存中有多少个User对象：</span></p>
  <p data-lake-id="u292578e3" id="u292578e3"><br></p>
  <pre lang="java"><code>
➜  ~ jps
709
2858 Launcher
2859 StackAllocTest
2860 Jps
➜  ~ jmap -histo 2859

 num     #instances         #bytes  class name
----------------------------------------------
   1:           524      101944280  [I
   2:          6806        2093136  [B
   3:         83619        1337904  StackAllocTest$User
   4:          8006        1320872  [C
   5:          4188         100512  java.lang.String
   6:           581          66304  java.lang.Class
</code></pre>
  <p data-lake-id="u8197d27f" id="u8197d27f"><br></p>
  <p data-lake-id="u0627abd3" id="u0627abd3"><span data-lake-id="ue13ecc70" id="ue13ecc70">从以上打印结果中可以发现，开启了逃逸分析之后（-XX:+DoEscapeAnalysis），在堆内存中只有8万多个</span><code data-lake-id="ucfb0a00c" id="ucfb0a00c"><span data-lake-id="u59abb726" id="u59abb726">StackAllocTest$User</span></code><span data-lake-id="u07c4eb08" id="u07c4eb08">对象。也就是说在经过JIT优化之后，堆内存中分配的对象数量，从100万降到了8万。</span></p>
  <p data-lake-id="ucf59b44a" id="ucf59b44a"><br></p>
  <blockquote data-lake-id="ud79f4aff" id="ud79f4aff">
   <p data-lake-id="u6f605988" id="u6f605988"><span data-lake-id="u64fb7931" id="u64fb7931">除了以上通过jmap验证对象个数的方法以外，读者还可以尝试将堆内存调小，然后执行以上代码，根据GC的次数来分析，也能发现，开启了逃逸分析之后，在运行期间，GC次数会明显减少。正是因为很多堆上分配被优化成了栈上分配，所以GC次数有了明显的减少。</span></p>
  </blockquote>
  <p data-lake-id="u0c41e737" id="u0c41e737"><br></p>
  <h2 data-lake-id="JtJu0" id="JtJu0"><span data-lake-id="u3e2f2685" id="u3e2f2685">​</span><br></h2>
 </body>
</html>